package me.gujun.ohos.taggroup;


import ohos.agp.components.*;
import ohos.agp.render.*;
import ohos.agp.utils.Color;
import ohos.agp.utils.Rect;
import ohos.agp.utils.RectFloat;
import ohos.agp.utils.TextAlignment;
import ohos.app.Context;
import ohos.multimodalinput.event.KeyEvent;
import ohos.multimodalinput.event.TouchEvent;

import java.util.ArrayList;
import java.util.List;

/**
 * A <code>TagGroup</code> is a special layout with a set of tags.
 * This group has two modes:
 * <p>
 * 1. APPEND mode
 * 2. DISPLAY mode
 * </p>
 * Default is DISPLAY mode. When in APPEND mode, the group is capable of input for append new tags
 * and delete tags.
 * <p>
 * When in DISPLAY mode, the group is only contain NORMAL state tags, and the tags in group
 * is not focusable.
 * </p>
 *
 * @author Jun Gu (http://2dxgujun.com)
 * @version 2.0
 * @since 2015-2-3 14:16:32
 */
public class TagGroup extends ComponentContainer implements Component.DrawTask, Component.LayoutRefreshedListener {
    private final int default_border_color = Color.rgb(0x49, 0xC1, 0x20);
    private final int default_text_color = Color.rgb(0x49, 0xC1, 0x20);
    private final int default_background_color = Color.WHITE.getValue();
    private final int default_dash_border_color = Color.rgb(0xAA, 0xAA, 0xAA);
    private final int default_input_hint_color = Color.argb(0x80, 0x00, 0x00, 0x00);
    private final int default_input_text_color = Color.argb(0xDE, 0x00, 0x00, 0x00);
    private final int default_checked_border_color = Color.rgb(0x49, 0xC1, 0x20);
    private final int default_checked_text_color = Color.WHITE.getValue();
    private final int default_checked_marker_color = Color.WHITE.getValue();
    private final int default_checked_background_color = Color.rgb(0x49, 0xC1, 0x20);
    private final int default_pressed_background_color = Color.rgb(0xED, 0xED, 0xED);
    private final float default_border_stroke_width;
    private final float default_text_size;
    private final float default_horizontal_spacing;
    private final float default_vertical_spacing;
    private final float default_horizontal_padding;
    private final float default_vertical_padding;

    /**
     * Indicates whether this TagGroup is set up to APPEND mode or DISPLAY mode. Default is false.
     */
    private boolean isAppendMode;

    /**
     * The text to be displayed when the text of the INPUT tag is empty.
     */
    private CharSequence inputHint;

    /**
     * The tag outline border color.
     */
    private int borderColor;

    /**
     * The tag text color.
     */
    private int textColor;

    /**
     * The tag background color.
     */
    private int backgroundColor;

    /**
     * The dash outline border color.
     */
    private int dashBorderColor;

    /**
     * The  input tag hint text color.
     */
    private int inputHintColor;

    /**
     * The input tag type text color.
     */
    private int inputTextColor;

    /**
     * The checked tag outline border color.
     */
    private int checkedBorderColor;

    /**
     * The check text color
     */
    private int checkedTextColor;

    /**
     * The checked marker color.
     */
    private int checkedMarkerColor;

    /**
     * The checked tag background color.
     */
    private int checkedBackgroundColor;

    /**
     * The tag background color, when the tag is being pressed.
     */
    private int pressedBackgroundColor;

    /**
     * The tag outline border stroke width, default is 0.5dp.
     */
    private float borderStrokeWidth;

    /**
     * The tag text size, default is 13sp.
     */
    private float textSize;

    /**
     * The horizontal tag spacing, default is 8.0dp.
     */
    private int horizontalSpacing;

    /**
     * The vertical tag spacing, default is 4.0dp.
     */
    private int verticalSpacing;

    /**
     * The horizontal tag padding, default is 12.0dp.
     */
    private int horizontalPadding;

    /**
     * The vertical tag padding, default is 3.0dp.
     */
    private int verticalPadding;

    /**
     * Listener used to dispatch tag change event.
     */
    private OnTagChangeListener mOnTagChangeListener;

    /**
     * Listener used to dispatch tag click event.
     */
    private OnTagClickListener mOnTagClickListener;

    private boolean isInit;

    private boolean isWidthContent;
    private boolean isHeightContent;

    /**
     * Listener used to handle tag click event.
     */
    private InternalTagClickListener mInternalTagClickListener = new InternalTagClickListener();

    public TagGroup(Context context) {
        this(context, null);
    }

    public TagGroup(Context context, AttrSet attrs) {
        this(context, attrs, null);
    }

    public TagGroup(Context context, AttrSet attrs, String defStyleAttr) {
        super(context, attrs, defStyleAttr);
        default_border_stroke_width = dp2px(0.5f);
        default_text_size = sp2px(13.0f);
        default_horizontal_spacing = dp2px(8.0f);
        default_vertical_spacing = dp2px(4.0f);
        default_horizontal_padding = dp2px(12.0f);
        default_vertical_padding = dp2px(3.0f);

        // Load styled attributes.
        isAppendMode = AttrUtils.getBooleanFromAttr(attrs, "atg_isAppendMode", false);
        inputHint = AttrUtils.getStringFromAttr(attrs, "atg_inputHint", null);
        borderColor = AttrUtils.getColorFromAttr(attrs, "atg_borderColor", default_border_color);
        textColor = AttrUtils.getColorFromAttr(attrs, "atg_textColor", default_text_color);
        backgroundColor = AttrUtils.getColorFromAttr(attrs, "atg_backgroundColor", default_background_color);
        dashBorderColor = AttrUtils.getColorFromAttr(attrs, "atg_dashBorderColor", default_dash_border_color);
        inputHintColor = AttrUtils.getColorFromAttr(attrs, "atg_inputHintColor", default_input_hint_color);
        inputTextColor = AttrUtils.getColorFromAttr(attrs, "atg_inputTextColor", default_input_text_color);
        checkedBorderColor = AttrUtils.getColorFromAttr(attrs, "atg_checkedBorderColor", default_checked_border_color);
        checkedTextColor = AttrUtils.getColorFromAttr(attrs, "atg_checkedTextColor", default_checked_text_color);
        checkedMarkerColor = AttrUtils.getColorFromAttr(attrs, "atg_checkedMarkerColor", default_checked_marker_color);
        checkedBackgroundColor = AttrUtils.getColorFromAttr(attrs, "atg_checkedBackgroundColor", default_checked_background_color);
        pressedBackgroundColor = AttrUtils.getColorFromAttr(attrs, "atg_pressedBackgroundColor", default_pressed_background_color);
        borderStrokeWidth = AttrUtils.getDimensionFromAttr(attrs, "atg_borderStrokeWidth", (int) default_border_stroke_width);
        textSize = AttrUtils.getDimensionFromAttr(attrs, "atg_textSize", (int) default_text_size);
        horizontalSpacing = AttrUtils.getDimensionFromAttr(attrs, "atg_horizontalSpacing", (int) default_horizontal_spacing);
        verticalSpacing = AttrUtils.getDimensionFromAttr(attrs, "atg_verticalSpacing", (int) default_vertical_spacing);
        horizontalPadding = AttrUtils.getDimensionFromAttr(attrs, "atg_horizontalPadding", (int) default_horizontal_padding);
        verticalPadding = AttrUtils.getDimensionFromAttr(attrs, "atg_verticalPadding", (int) default_vertical_padding);

        if (isAppendMode) {
            // Append the initial INPUT tag.

            appendInputTag();

            // Set the click listener to detect the end-input event.
            setClickedListener(new ClickedListener() {
                @Override
                public void onClick(Component component) {
                    submitTag();
                }
            });

        }
        addDrawTask(this);
        setLayoutRefreshedListener(this);

        LayoutConfig layoutConfig = getLayoutConfig();
        isWidthContent = layoutConfig.width == LayoutConfig.MATCH_CONTENT;
        isHeightContent = layoutConfig.height == LayoutConfig.MATCH_CONTENT;
    }

    /**
     * Call this to submit the INPUT tag.
     */
    public void submitTag() {
        final TagView inputTag = getInputTag();
        if (inputTag != null && inputTag.isInputAvailable()) {
            inputTag.endInput();

            if (mOnTagChangeListener != null) {
                mOnTagChangeListener.onAppend(TagGroup.this, inputTag.getText().toString());
            }
            appendInputTag();
        }
    }

    @Override
    public void onDraw(Component component, Canvas canvas) {
        onRefreshed(component);
    }

    private int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
        final int specMode = EstimateSpec.getMode(measureSpec);
        final int specSize = EstimateSpec.getSize(measureSpec);
        final int result;
        switch (specMode) {
            case EstimateSpec.NOT_EXCEED:
                if (specSize < size) {
                    result = specSize | 0x01000000;
                } else {
                    result = size;
                }
                break;
            case EstimateSpec.PRECISE:
                result = specSize;
                break;
            case EstimateSpec.UNCONSTRAINT:
            default:
                result = size;
        }
        return result | (childMeasuredState & 0xff000000);
    }

    private int measureDimension(int defaultSize, int measureSpec) {
        int result = defaultSize;
        int specMode = EstimateSpec.getMode(measureSpec);
        int specSize = EstimateSpec.getSize(measureSpec);
        if (specMode == EstimateSpec.PRECISE) {
            result = specSize;
        } else if (specMode == EstimateSpec.NOT_EXCEED) {
            result = Math.min(defaultSize, specSize);
        }
        return result;
    }

    private int calculateWidth(int maxWidth) {
        int result = 0;
        final int parentLeft = getPaddingLeft();
        final int parentRight = maxWidth - getPaddingLeft() - getPaddingRight();
        final int count = getChildCount();
        int childLeft = parentLeft;
        for (int i = 0; i < count; i++) {
            final Component child = getComponentAt(i);
            final int width = child.getWidth();

            if (child.getVisibility() != HIDE) {
                if (childLeft + width > parentRight) { // Next line
                    return maxWidth;
                }
                childLeft += width + horizontalSpacing;
                result = childLeft;
            }
        }
        return result;
    }

    private int calculateHeight(int maxWidth, int maxHeight) {
        int result = 0;
        final int parentLeft = getPaddingLeft();
        final int parentRight = maxWidth - getPaddingLeft() - getPaddingRight();
        final int parentTop = getPaddingTop();

        int childLeft = parentLeft;
        int childTop = parentTop;

        int rowMaxHeight = 0;

        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            final Component child = getComponentAt(i);
            final int width = child.getWidth();
            final int height = child.getHeight();

            if (child.getVisibility() != HIDE) {
                if (childLeft + width > parentRight) { // Next line
                    childLeft = parentLeft;
                    childTop += rowMaxHeight + verticalSpacing;
                    rowMaxHeight = height;
                } else {
                    rowMaxHeight = Math.max(rowMaxHeight, height);
                }
                childLeft += width + horizontalSpacing;
            }
            result = childTop + height;
            if (rowMaxHeight > maxHeight) {
                return maxHeight;
            }
        }
        return result;
    }

    @Override
    public void onRefreshed(Component component) {
        int maxWidth = getWidth();
        int maxHeight = getHeight();
        if (isWidthContent && getChildCount() > 1) {
            ComponentParent componentParent = getComponentParent();
            if (componentParent != null && componentParent instanceof ComponentContainer) {
                maxWidth = ((ComponentContainer) componentParent).getWidth();
                setWidth(calculateWidth(maxWidth));
            }
        }
        if (isHeightContent && getChildCount() > 1) {
            ComponentParent componentParent = getComponentParent();
            if (componentParent != null && componentParent instanceof ComponentContainer) {
                maxHeight = ((ComponentContainer) componentParent).getHeight();
                setHeight(calculateHeight(maxWidth, maxHeight));
            }
        }

        final int parentLeft = getPaddingLeft();
        final int parentRight = getWidth() - getPaddingLeft() - getPaddingRight();
        final int parentTop = getPaddingTop();
        final int parentBottom = getHeight() - getPaddingTop() - getPaddingBottom();

        int childLeft = parentLeft;
        int childTop = parentTop;

        int rowMaxHeight = 0;

        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            final Component child = getComponentAt(i);
            final int width = child.getWidth();
            final int height = child.getHeight();

            if (child.getVisibility() != HIDE) {
                if (childLeft + width > parentRight) { // Next line
                    childLeft = parentLeft;
                    childTop += rowMaxHeight + verticalSpacing;
                    rowMaxHeight = height;
                } else {
                    rowMaxHeight = Math.max(rowMaxHeight, height);
                }
                child.setContentPosition(childLeft, childTop);
//                child.arrange(childLeft, childTop, childLeft + width, childTop + height);

                childLeft += width + horizontalSpacing;
            }
        }
    }

    /**
     * Returns the INPUT tag view in this group.
     *
     * @return the INPUT state tag view or null if not exists
     */
    protected TagView getInputTag() {
        if (isAppendMode) {
            final int inputTagIndex = getChildCount() - 1;
            final TagView inputTag = getTagAt(inputTagIndex);
            if (inputTag != null && inputTag.mState == TagView.STATE_INPUT) {
                return inputTag;
            } else {
                return null;
            }
        } else {
            return null;
        }
    }

    /**
     * Returns the INPUT state tag in this group.
     *
     * @return the INPUT state tag view or null if not exists
     */
    public String getInputTagText() {
        final TagView inputTagView = getInputTag();
        if (inputTagView != null) {
            return inputTagView.getText().toString();
        }
        return null;
    }

    /**
     * Return the last NORMAL state tag view in this group.
     *
     * @return the last NORMAL state tag view or null if not exists
     */
    protected TagView getLastNormalTagView() {
        final int lastNormalTagIndex = isAppendMode ? getChildCount() - 2 : getChildCount() - 1;
        TagView lastNormalTagView = getTagAt(lastNormalTagIndex);
        return lastNormalTagView;
    }

    /**
     * Returns the tag array in group, except the INPUT tag.
     *
     * @return the tag array.
     */
    public String[] getTags() {
        final int count = getChildCount();
        final List<String> tagList = new ArrayList<>();
        for (int i = 0; i < count; i++) {
            final TagView tagView = getTagAt(i);
            if (tagView.mState == TagView.STATE_NORMAL) {
                tagList.add(tagView.getText().toString());
            }
        }

        return tagList.toArray(new String[tagList.size()]);
    }

    /**
     * setTags
     *
     * @param tagList the tag list
     * @see #setTags(String...)
     */
    public void setTags(List<String> tagList) {
        setTags(tagList.toArray(new String[tagList.size()]));
    }

    /**
     * Set the tags. It will remove all previous tags first.
     *
     * @param tags the tag list to set.
     */
    public void setTags(String... tags) {
        removeAllComponents();
        for (final String tag : tags) {
            appendTag(tag);
        }

        if (isAppendMode) {
            appendInputTag();
        }
    }

    /**
     * Returns the tag view at the specified position in the group.
     *
     * @param index the position at which to get the tag view from.
     * @return the tag view at the specified position or null if the position
     * does not exists within this group.
     */
    protected TagView getTagAt(int index) {
        return (TagView) getComponentAt(index);
    }

    /**
     * Returns the checked tag view in the group.
     *
     * @return the checked tag view or null if not exists.
     */
    protected TagView getCheckedTag() {
        final int checkedTagIndex = getCheckedTagIndex();
        if (checkedTagIndex != -1) {
            return getTagAt(checkedTagIndex);
        }
        return null;
    }

    /**
     * Return the checked tag index.
     *
     * @return the checked tag index, or -1 if not exists.
     */
    protected int getCheckedTagIndex() {
        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            final TagView tag = getTagAt(i);
            if (tag.isChecked) {
                return i;
            }
        }
        return -1;
    }

    /**
     * Register a callback to be invoked when this tag group is changed.
     *
     * @param l the callback that will run
     */
    public void setOnTagChangeListener(OnTagChangeListener l) {
        mOnTagChangeListener = l;
    }

    /**
     * append tag
     *
     * @see #appendInputTag(String)
     */
    protected void appendInputTag() {
        appendInputTag(null);
    }

    /**
     * Append a INPUT tag to this group. It will throw an exception if there has a previous INPUT tag.
     *
     * @param tag the tag text.
     */
    protected void appendInputTag(String tag) {
        final TagView previousInputTag = getInputTag();
        if (previousInputTag != null) {
            throw new IllegalStateException("Already has a INPUT tag in group.");
        }

        final TagView newInputTag = new TagView(getContext(), TagView.STATE_INPUT, tag);
        newInputTag.setClickedListener(mInternalTagClickListener);
        addComponent(newInputTag);
    }

    /**
     * Append tag to this group.
     *
     * @param tag the tag to append.
     */
    protected void appendTag(CharSequence tag) {
        final TagView newTag = new TagView(getContext(), TagView.STATE_NORMAL, tag);
        newTag.setClickedListener(mInternalTagClickListener);
        addComponent(newTag);
    }

    private float dp2px(float dp) {
        return (int) (getResourceManager().getDeviceCapability().screenDensity / 160 * dp);
    }

    private float sp2px(float sp) {
        return (int) (getResourceManager().getDeviceCapability().screenDensity / 160 * sp);
    }

    @Override
    public ComponentContainer.LayoutConfig createLayoutConfig(Context context, AttrSet attrs) {
        return new LayoutParams(context, attrs);
    }

    /**
     * Register a callback to be invoked when a tag is clicked.
     *
     * @param l the callback that will run.
     */
    public void setOnTagClickListener(OnTagClickListener l) {
        mOnTagClickListener = l;
    }

    protected void deleteTag(TagView tagView) {
        removeComponent(tagView);
        if (mOnTagChangeListener != null) {
            mOnTagChangeListener.onDelete(TagGroup.this, tagView.getText().toString());
        }
    }


    /**
     * Interface definition for a callback to be invoked when a tag group is changed.
     */
    public interface OnTagChangeListener {
        /**
         * Called when a tag has been appended to the group.
         *
         * @param tagGroup the tagGroup
         * @param tag      the appended tag.
         */
        void onAppend(TagGroup tagGroup, String tag);

        /**
         * Called when a tag has been deleted from the the group.
         *
         * @param tagGroup the tagGroup
         * @param tag      the deleted tag.
         */
        void onDelete(TagGroup tagGroup, String tag);
    }

    /**
     * Interface definition for a callback to be invoked when a tag is clicked.
     */
    public interface OnTagClickListener {
        /**
         * Called when a tag has been clicked.
         *
         * @param tag The tag text of the tag that was clicked.
         */
        void onTagClick(String tag);
    }

    /**
     * Per-child layout information for layouts.
     */
    public static class LayoutParams extends ComponentContainer.LayoutConfig {
        public LayoutParams(Context c, AttrSet attrs) {
            super(c, attrs);
        }

        public LayoutParams(int width, int height) {
            super(width, height);
        }
    }

    /**
     * The tag view click listener for internal use.
     */
    class InternalTagClickListener implements ClickedListener {
        @Override
        public void onClick(Component v) {
            final TagView tag = (TagView) v;
            if (isAppendMode) {
                if (tag.mState == TagView.STATE_INPUT) {
                    // If the clicked tag is in INPUT state, uncheck the previous checked tag if exists.
                    final TagView checkedTag = getCheckedTag();
                    if (checkedTag != null) {
                        checkedTag.setChecked(false);
                    }

                } else {
                    // If the clicked tag is currently checked, delete the tag.
                    if (tag.isChecked) {
                        deleteTag(tag);
                    } else {
                        // If the clicked tag is unchecked, uncheck the previous checked tag if exists,
                        // then check the clicked tag.
                        final TagView checkedTag = getCheckedTag();
                        if (checkedTag != null) {
                            checkedTag.setChecked(false);
                        }
                        tag.setChecked(true);

                    }

                }
            } else {
                if (mOnTagClickListener != null) {
                    mOnTagClickListener.onTagClick(tag.getText().toString());
                }
            }
        }
    }

    /**
     * The tag view which has two states can be either NORMAL or INPUT.
     */
    class TagView extends TextField implements DrawTask, LayoutRefreshedListener, TouchEventListener {
        public static final int STATE_NORMAL = 1;
        public static final int STATE_INPUT = 2;

        /**
         * The offset to the text.
         */
        private static final int CHECKED_MARKER_OFFSET = 3;

        /**
         * The stroke width of the checked marker
         */
        private static final int CHECKED_MARKER_STROKE_WIDTH = 4;

        /**
         * The current state.
         */
        private int mState;

        /**
         * Indicates the tag if checked.
         */
        private boolean isChecked = false;

        /**
         * Indicates the tag if pressed.
         */
        private boolean isPressed = false;

        private Paint mBorderPaint = new Paint();

        private Paint mBackgroundPaint = new Paint();

        private Paint mCheckedMarkerPaint = new Paint();

        /**
         * The rect for the tag's left corner drawing.
         */
        private RectFloat mLeftCornerRectF = new RectFloat();

        /**
         * The rect for the tag's right corner drawing.
         */
        private RectFloat mRightCornerRectF = new RectFloat();

        /**
         * The rect for the tag's horizontal blank fill area.
         */
        private RectFloat mHorizontalBlankFillRectF = new RectFloat();

        /**
         * The rect for the tag's vertical blank fill area.
         */
        private RectFloat mVerticalBlankFillRectF = new RectFloat();

        /**
         * The rect for the checked mark draw bound.
         */
        private RectFloat mCheckedMarkerBound = new RectFloat();

        /**
         * Used to detect the touch event.
         */
        private Rect mOutRect = new Rect();

        /**
         * The path for draw the tag's outline border.
         */
        private Path mBorderPath = new Path();

        /**
         * The path effect provide draw the dash border.
         */
        private PathEffect mPathEffect = new PathEffect(new float[]{10, 5}, 0);

        {
            mBorderPaint.setStyle(Paint.Style.STROKE_STYLE);
            mBorderPaint.setStrokeWidth(borderStrokeWidth);
            mBackgroundPaint.setStyle(Paint.Style.FILL_STYLE);
            mCheckedMarkerPaint.setStyle(Paint.Style.FILL_STYLE);
            mCheckedMarkerPaint.setStrokeWidth(CHECKED_MARKER_STROKE_WIDTH);
            mCheckedMarkerPaint.setColor(new Color(checkedMarkerColor));
        }


        public TagView(Context context, final int state, CharSequence text) {
            super(context);
            addDrawTask(this, DrawTask.BETWEEN_BACKGROUND_AND_CONTENT);
            setLayoutRefreshedListener(this);
            setTouchEventListener(this);
            setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding);
            setLayoutConfig(new LayoutConfig(LayoutConfig.MATCH_CONTENT, LayoutConfig.MATCH_CONTENT));
            setTextAlignment(TextAlignment.CENTER);
            if (text != null) {
                setText(text.toString());
            }
            setTextSize((int) textSize, TextSizeType.PX);

            mState = state;

            setClickable(isAppendMode);
            setFocusable(state == STATE_INPUT ? FOCUS_ENABLE : FOCUS_DISABLE);
            setTouchFocusable(state == STATE_INPUT);
            setHint(state == STATE_INPUT ? inputHint.toString() : null);
//            setEnabled(state == STATE_INPUT?true:false);

            if (state == STATE_INPUT) {
                requestFocus();

                // Handle the ENTER key down.
                // Handle the BACKSPACE key down.
                setKeyEventListener(new KeyEventListener() {
                    @Override
                    public boolean onKeyEvent(Component component, KeyEvent keyEvent) {
                        int keyCode = keyEvent.getKeyCode();
                        if (keyCode == KeyEvent.KEY_DEL && keyEvent.isKeyDown()) {
                            // If the input content is empty, check or remove the last NORMAL state tag.
                            if (getText() == null || getText().length() == 0) {
                                TagView lastNormalTagView = getLastNormalTagView();
                                if (lastNormalTagView != null) {
                                    if (lastNormalTagView.isChecked) {
                                        removeComponent(lastNormalTagView);
                                        if (mOnTagChangeListener != null) {
                                            mOnTagChangeListener.onDelete(TagGroup.this, lastNormalTagView.getText().toString());
                                        }
                                    } else {
                                        final TagView checkedTagView = getCheckedTag();
                                        if (checkedTagView != null) {
                                            checkedTagView.setChecked(false);
                                        }
                                        lastNormalTagView.setChecked(true);
                                    }
                                    lastNormalTagView.invalidate();
                                    return true;
                                }
                            }
                        } else if (keyCode == KeyEvent.KEY_ENTER && keyEvent.isKeyDown()) {
                            if (isInputAvailable()) {
                                // If the input content is available, end the input and dispatch
                                // the event, then append a new INPUT state tag.
                                endInput();
                                if (mOnTagChangeListener != null) {
                                    mOnTagChangeListener.onAppend(TagGroup.this, getText().toString());
                                }
                                appendInputTag();
                                invalidate();
                            }
                            return true;
                        }
                        return false;
                    }
                });

                // Handle the INPUT tag content changed.
                addTextObserver(new TextObserver() {
                    @Override
                    public void onTextUpdated(String s, int i, int i1, int i2) {
                        final TagView checkedTagView = getCheckedTag();
                        if (checkedTagView != null) {
                            checkedTagView.setChecked(false);
                        }
                        invalidate();
                    }
                });
            }

            invalidatePaint();
        }

        /**
         * Set whether this tag view is in the checked state.
         *
         * @param checked true is checked, false otherwise
         */
        public void setChecked(boolean checked) {
            isChecked = checked;
            // Make the checked mark drawing region.
            setPadding(horizontalPadding,
                    verticalPadding,
                    isChecked ? (int) (horizontalPadding + getHeight() / 2.5f + CHECKED_MARKER_OFFSET)
                            : horizontalPadding,
                    verticalPadding);
            invalidatePaint();
            invalidate();
        }

        /**
         * Call this method to end this tag's INPUT state.
         */
        public void endInput() {
            // Make the view not focusable.
            setFocusable(FOCUS_DISABLE);
            setTouchFocusable(false);
            // Set the hint empty, make the TextView measure correctly.
            setHint(null);
            // Take away the cursor.
//            setEnabled(false);

            mState = STATE_NORMAL;
            invalidatePaint();
            postLayout();
        }

        /**
         * Indicates whether the input content is available.
         *
         * @return True if the input content is available, false otherwise.
         */
        public boolean isInputAvailable() {
            return getText() != null && getText().length() > 0;
        }

        private void invalidatePaint() {
            if (isAppendMode) {
                if (mState == STATE_INPUT) {
                    mBorderPaint.setColor(new Color(dashBorderColor));
                    mBorderPaint.setPathEffect(mPathEffect);
                    mBackgroundPaint.setColor(new Color(backgroundColor));
                    setHintColor(new Color(inputHintColor));
                    setTextColor(new Color(inputTextColor));
                } else {
                    mBorderPaint.setPathEffect(null);
                    if (isChecked) {
                        mBorderPaint.setColor(new Color(checkedBorderColor));
                        mBackgroundPaint.setColor(new Color(checkedBackgroundColor));
                        setTextColor(new Color(checkedTextColor));
                    } else {
                        mBorderPaint.setColor(new Color(borderColor));
                        mBackgroundPaint.setColor(new Color(backgroundColor));
                        setTextColor(new Color(textColor));
                    }
                }
            } else {
                mBorderPaint.setColor(new Color(borderColor));
                mBackgroundPaint.setColor(new Color(backgroundColor));
                setTextColor(new Color(textColor));
            }

            if (isPressed) {
                mBackgroundPaint.setColor(new Color(pressedBackgroundColor));
            }
        }

        @Override
        public void onDraw(Component component, Canvas canvas) {
            invalidatePaint();
            onRefreshed(component);
            canvas.drawArc(mLeftCornerRectF, new Arc(-180, 90, true), mBackgroundPaint);
            canvas.drawArc(mLeftCornerRectF, new Arc(-270, 90, true), mBackgroundPaint);
            canvas.drawArc(mRightCornerRectF, new Arc(-90, 90, true), mBackgroundPaint);
            canvas.drawArc(mRightCornerRectF, new Arc(0, 90, true), mBackgroundPaint);
            canvas.drawRect(mHorizontalBlankFillRectF, mBackgroundPaint);
            canvas.drawRect(mVerticalBlankFillRectF, mBackgroundPaint);

            if (isChecked) {
                canvas.save();
                canvas.rotate(45, mCheckedMarkerBound.getHorizontalCenter(), mCheckedMarkerBound.getVerticalCenter());
                canvas.drawLine(mCheckedMarkerBound.left, mCheckedMarkerBound.getVerticalCenter(),
                        mCheckedMarkerBound.right, mCheckedMarkerBound.getVerticalCenter(), mCheckedMarkerPaint);
                canvas.drawLine(mCheckedMarkerBound.getHorizontalCenter(), mCheckedMarkerBound.top,
                        mCheckedMarkerBound.getHorizontalCenter(), mCheckedMarkerBound.bottom, mCheckedMarkerPaint);
                canvas.restore();
            }
            canvas.drawPath(mBorderPath, mBorderPaint);
        }

        @Override
        public void onRefreshed(Component component) {
            int w = component.getWidth();
            int h = component.getHeight();
            int left = (int) borderStrokeWidth;
            int top = (int) borderStrokeWidth;
            int right = (int) (left + w - borderStrokeWidth * 2);
            int bottom = (int) (top + h - borderStrokeWidth * 2);

            int d = bottom - top;

            mLeftCornerRectF.modify(left, top, left + d, top + d);
            mRightCornerRectF.modify(right - d, top, right, top + d);

            mBorderPath.reset();
            mBorderPath.addArc(mLeftCornerRectF, -180, 90);
            mBorderPath.addArc(mLeftCornerRectF, -270, 90);
            mBorderPath.addArc(mRightCornerRectF, -90, 90);
            mBorderPath.addArc(mRightCornerRectF, 0, 90);

            int l = (int) (d / 2.0f);
            mBorderPath.moveTo(left + l, top);
            mBorderPath.lineTo(right - l, top);

            mBorderPath.moveTo(left + l, bottom);
            mBorderPath.lineTo(right - l, bottom);

            mBorderPath.moveTo(left, top + l);
            mBorderPath.lineTo(left, bottom - l);

            mBorderPath.moveTo(right, top + l);
            mBorderPath.lineTo(right, bottom - l);

            mHorizontalBlankFillRectF.modify(left, top + l, right, bottom - l);
            mVerticalBlankFillRectF.modify(left + l, top, right - l, bottom);

            int m = (int) (h / 2.5f);
            h = bottom - top;
            mCheckedMarkerBound.modify(right - m - horizontalPadding + CHECKED_MARKER_OFFSET,
                    top + h / 2 - m / 2,
                    right - horizontalPadding + CHECKED_MARKER_OFFSET,
                    bottom - h / 2 + m / 2);

            // Ensure the checked mark drawing region is correct across screen orientation changes.
            if (isChecked) {
                setPadding(horizontalPadding,
                        verticalPadding,
                        (int) (horizontalPadding + h / 2.5f + CHECKED_MARKER_OFFSET),
                        verticalPadding);
            }
        }

        @Override
        public boolean onTouchEvent(Component component, TouchEvent touchEvent) {
            if (mState == STATE_INPUT) {
                // The INPUT tag doesn't change background color on the touch event.
                return false;
            }

            switch (touchEvent.getAction()) {
                case TouchEvent.PRIMARY_POINT_DOWN: {
                    mOutRect = new Rect(0, 0, getWidth(), getHeight());
                    isPressed = true;
                    invalidate();
                    break;
                }
                case TouchEvent.POINT_MOVE: {
                    if (getTouchX(touchEvent, 0, component) < 0 ||
                            getTouchX(touchEvent, 0, component) > getWidth() ||
                            getTouchY(touchEvent, 0, component) < 0 ||
                            getTouchY(touchEvent, 0, component) > getHeight()) {
                        isPressed = false;
                        invalidate();
                    }
                    break;
                }
                case TouchEvent.PRIMARY_POINT_UP: {
                    isPressed = false;
                    invalidate();
                    break;
                }
            }
            return true;
        }

    }

    private float getTouchX(TouchEvent touchEvent, int index, Component component) {
        float x = 0;
        if (touchEvent.getPointerCount() > index) {
            int[] xy = component.getLocationOnScreen();
            if (xy != null && xy.length == 2) {
                x = touchEvent.getPointerScreenPosition(index).getX() - xy[0];
            } else {
                x = touchEvent.getPointerPosition(index).getX();
            }
        }
        return x;
    }

    private float getTouchY(TouchEvent touchEvent, int index, Component component) {
        float y = 0;
        if (touchEvent.getPointerCount() > index) {
            int[] xy = component.getLocationOnScreen();
            if (xy != null && xy.length == 2) {
                y = touchEvent.getPointerScreenPosition(index).getY() - xy[1];
            } else {
                y = touchEvent.getPointerPosition(index).getY();
            }
        }
        return y;
    }
}